/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * XMLExperiment.java
 * Copyright (C) 2004-2012 University of Waikato, Hamilton, New Zealand
 */

package weka.experiment.xml;

import java.beans.PropertyDescriptor;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Vector;

import org.w3c.dom.Element;

import weka.core.xml.XMLBasicSerialization;
import weka.core.xml.XMLDocument;
import weka.experiment.Experiment;
import weka.experiment.PropertyNode;

/**
 * This class serializes and deserializes an Experiment instance to and fro
 * XML.<br>
 * It omits the <code>options</code> from the Experiment, since these are
 * handled by the get/set-methods. For the <code>Classifier</code> class with
 * all its derivative classes it stores only <code>debug</code> and
 * <code>options</code>. For <code>SplitEvaluator</code> and
 * <code>ResultProducer</code> only the options are retrieved. The
 * <code>PropertyNode</code> is done manually since it has no get/set-methods
 * for its public fields.<br>
 * Since there's no read-method for <code>m_ClassFirst</code> we always save it
 * as <code>false</code>.
 * 
 * @see Experiment#m_ClassFirst
 * 
 * @author FracPete (fracpete at waikato dot ac dot nz)
 * @version $Revision$
 */
public class XMLExperiment extends XMLBasicSerialization {

    /** the name of the classFirst property */
    public final static String NAME_CLASSFIRST = "classFirst";

    /** PropertyNode member */
    public final static String NAME_PROPERTYNODE_VALUE = "value";

    /** PropertyNode member */
    public final static String NAME_PROPERTYNODE_PARENTCLASS = "parentClass";

    /** PropertyNode member */
    public final static String NAME_PROPERTYNODE_PROPERTY = "property";

    /**
     * initializes the serialization
     * 
     * @throws Exception if initialization fails
     */
    public XMLExperiment() throws Exception {
        super();
    }

    /**
     * generates internally a new XML document and clears also the IgnoreList and
     * the mappings for the Read/Write-Methods
     * 
     * @throws Exception if initializing fails
     */
    @Override
    public void clear() throws Exception {
        super.clear();

        // ignore
        m_Properties.addIgnored(VAL_ROOT + ".options");
        m_Properties.addIgnored(Experiment.class, "options");

        // allow
        m_Properties.addAllowed(weka.classifiers.Classifier.class, "debug");
        m_Properties.addAllowed(weka.classifiers.Classifier.class, "options");
        // we assume that classes implementing SplitEvaluator also implement
        // OptionHandler
        m_Properties.addAllowed(weka.experiment.SplitEvaluator.class, "options");
        // we assume that classes implementing ResultProducer also implement
        // OptionHandler
        m_Properties.addAllowed(weka.experiment.ResultProducer.class, "options");

        // read/write methods
        m_CustomMethods.register(this, PropertyNode.class, "PropertyNode");
    }

    /**
     * enables derived classes to add other properties to the DOM tree, e.g. ones
     * that do not apply to the get/set convention of beans.
     * 
     * @param o the object that is serialized into XML
     * @throws Exception if post-processing fails
     */
    @Override
    protected void writePostProcess(Object o) throws Exception {
        Element node;

        // classfirst
        node = addElement(m_Document.getDocument().getDocumentElement(), NAME_CLASSFIRST, Boolean.class.getName(), false);
        node.appendChild(node.getOwnerDocument().createTextNode(new Boolean(false).toString())); // TODO: get-Method for classFirst in
                                                                                                 // Experiment???
    }

    /**
     * additional post-processing can happen in derived classes after reading from
     * XML.
     * 
     * @param o the object to perform some additional processing on
     * @return the processed object
     * @throws Exception if post-processing fails
     */
    @Override
    protected Object readPostProcess(Object o) throws Exception {
        Element node;
        Experiment exp;
        int i;
        Vector<Element> children;

        exp = (Experiment) o;

        // classfirst
        children = XMLDocument.getChildTags(m_Document.getDocument().getDocumentElement());
        for (i = 0; i < children.size(); i++) {
            node = children.get(i);
            if (node.getAttribute(ATT_NAME).equals(NAME_CLASSFIRST)) {
                exp.classFirst(new Boolean(XMLDocument.getContent(node)).booleanValue());
                break;
            }
        }

        return o;
    }

    /**
     * adds the given PropertyNode to a DOM structure.
     * 
     * @param parent the parent of this object, e.g. the class this object is a
     *               member of
     * @param o      the Object to describe in XML
     * @param name   the name of the object
     * @return the node that was created
     * @throws Exception if the DOM creation fails
     */
    public Element writePropertyNode(Element parent, Object o, String name) throws Exception {
        Element node;
        PropertyNode pnode;
        Vector<Element> children;
        int i;
        Element child;

        // for debugging only
        if (DEBUG) {
            trace(new Throwable(), name);
        }

        m_CurrentNode = parent;

        pnode = (PropertyNode) o;
        node = (Element) parent.appendChild(m_Document.getDocument().createElement(TAG_OBJECT));
        node.setAttribute(ATT_NAME, name);
        node.setAttribute(ATT_CLASS, pnode.getClass().getName());
        node.setAttribute(ATT_PRIMITIVE, VAL_NO);
        node.setAttribute(ATT_ARRAY, VAL_NO);

        if (pnode.value != null) {
            invokeWriteToXML(node, pnode.value, NAME_PROPERTYNODE_VALUE);
        }
        if (pnode.parentClass != null) {
            invokeWriteToXML(node, pnode.parentClass.getName(), NAME_PROPERTYNODE_PARENTCLASS);
        }
        if (pnode.property != null) {
            invokeWriteToXML(node, pnode.property.getDisplayName(), NAME_PROPERTYNODE_PROPERTY);
        }

        // fix primitive values
        if ((pnode.value != null) && (pnode.property != null) && (pnode.property.getPropertyType().isPrimitive())) {
            children = XMLDocument.getChildTags(node);
            for (i = 0; i < children.size(); i++) {
                child = children.get(i);
                if (!child.getAttribute(ATT_NAME).equals(NAME_PROPERTYNODE_VALUE)) {
                    continue;
                }
                child.setAttribute(ATT_CLASS, pnode.property.getPropertyType().getName());
                child.setAttribute(ATT_PRIMITIVE, VAL_YES);
            }
        }

        return node;
    }

    /**
     * builds the PropertyNode from the given DOM node.
     * 
     * @param node the associated XML node
     * @return the instance created from the XML description
     * @throws Exception if instantiation fails
     * @see javax.swing.DefaultListModel
     */
    public Object readPropertyNode(Element node) throws Exception {
        Object result;
        Object value;
        String parentClass;
        String property;
        Vector<Element> children;
        Element child;
        int i;
        Class<?> cls;

        // for debugging only
        if (DEBUG) {
            trace(new Throwable(), node.getAttribute(ATT_NAME));
        }

        m_CurrentNode = node;

        result = null;

        children = XMLDocument.getChildTags(node);
        value = null;
        parentClass = null;
        property = null;

        for (i = 0; i < children.size(); i++) {
            child = children.get(i);

            if (child.getAttribute(ATT_NAME).equals(NAME_PROPERTYNODE_VALUE)) {
                if (stringToBoolean(child.getAttribute(ATT_PRIMITIVE))) {
                    value = getPrimitive(child);
                } else {
                    value = invokeReadFromXML(child);
                }
            }
            if (child.getAttribute(ATT_NAME).equals(NAME_PROPERTYNODE_PARENTCLASS)) {
                parentClass = XMLDocument.getContent(child);
            }
            if (child.getAttribute(ATT_NAME).equals(NAME_PROPERTYNODE_PROPERTY)) {
                property = XMLDocument.getContent(child);
            }
        }

        if (parentClass != null) {
            cls = Class.forName(parentClass);
        } else {
            cls = null;
        }

        if (cls != null) {
            result = new PropertyNode(value, new PropertyDescriptor(property, cls), cls);
        } else {
            result = new PropertyNode(value);
        }

        return result;
    }

    /**
     * for testing only. if the first argument is a filename with ".xml" as
     * extension it tries to generate an instance from the XML description and does
     * a <code>toString()</code> of the generated object. Otherwise it loads the
     * binary file, saves the XML representation in a file with the original
     * filename appended by ".xml" and once again in a binary file with the original
     * filename appended by ".exp".
     * 
     * @param args the commandline arguments
     * @throws Exception if something goes wrong, e.g., file not found
     */
    public static void main(String[] args) throws Exception {
        if (args.length > 0) {
            // read xml and print
            if (args[0].toLowerCase().endsWith(".xml")) {
                System.out.println(new XMLExperiment().read(args[0]).toString());
            }
            // read binary and print generated XML
            else {
                // read
                FileInputStream fi = new FileInputStream(args[0]);
                ObjectInputStream oi = new ObjectInputStream(new BufferedInputStream(fi));
                Object o = oi.readObject();
                oi.close();
                // print to stdout
                // new XMLExperiment().write(System.out, o);
                // write to XML file
                new XMLExperiment().write(new BufferedOutputStream(new FileOutputStream(args[0] + ".xml")), o);
                // print to binary file
                FileOutputStream fo = new FileOutputStream(args[0] + ".exp");
                ObjectOutputStream oo = new ObjectOutputStream(new BufferedOutputStream(fo));
                oo.writeObject(o);
                oo.close();
            }
        }
    }
}
